2021-12-24
Heatmap.js 是目前应用最广的web动态热图javaScript库。heatmap使用 canvas 进行绘制。
一、传送门
Heatmap官网:https://www.patrick-wied.at/static/heatmapjs/
github下载: https://github.com/pa7/heatmap.js
二、代码结构
1、整个js库包裹在一个立即执行的匿名函数里,以避免污染全局命名空间。这也是很多js库的常见写法。2、核心对象有三个:Store(数据)、Canvas2dRenderer(绘制工具)、HeatMap(构建器)。3、通过global['h337']暴露创建工厂。
三、热力图渲染原理
以 heatmap.js v2.0.5 为例子; heatmap使用 canvas 进行绘制。
Heatmap.js 最重要的4个点: _getPointTemplate, _getColorPalette , _drawAlpha , _colorize
3.1、点模板 _getPointTemplate,设置单点渲染模板
点模板对应热力图数据点。它是一个圆点,根据可配置的模糊因子(blurFactor,默认.85),可使圆点带有模糊效果(借助createRadialGradient)。
主要是调用 canvas 的 createRadialGradient 方法。核心方法是canvas的createRadialGradient方法,每个点设置渲染半径,由渐变因子 blur 确定内圆比例,内圆与外圆的圆周间进行无色的放射渐变,达到中间透明度高,边缘透明度低的效果。这个无色的透明度渐变的圆形即为点的模板。
var _getPointTemplate = function(radius, blurFactor) {
var tplCanvas = document.createElement('canvas');
var tplCtx = tplCanvas.getContext('2d');
var x = radius;
var y = radius;
tplCanvas.width = tplCanvas.height = radius*2;
if (blurFactor == 1) {
tplCtx.beginPath();
tplCtx.arc(x, y, radius, 0, 2 * Math.PI, false);
tplCtx.fillStyle = 'rgba(0,0,0,1)';
tplCtx.fill();
} else {
var gradient = tplCtx.createRadialGradient(x, y, radius*blurFactor, x, y, radius);
gradient.addColorStop(0, 'rgba(0,0,0,1)');
gradient.addColorStop(1, 'rgba(0,0,0,0)');
tplCtx.fillStyle = gradient;
tplCtx.fillRect(0, 0, 2*radius, 2*radius);
}
return tplCanvas;
};
用 html canvas代码测试效果,代码如下:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
DOCTYPE html>
_getPointTemplate
/*canvas {border:1px solid black;}*/
window.onload = function () {
var container = document.querySelector('.heatmap');
var p1 = _getPointTemplate(40, 0.85);
//document.body.appendChild(p1);
container.appendChild(p1);
}
var _getPointTemplate = function(radius, blurFactor) {
var tplCanvas = document.createElement('canvas');
var tplCtx = tplCanvas.getContext('2d');
var x = radius;
var y = radius;
tplCanvas.width = tplCanvas.height = radius*2;
if (blurFactor == 1) {
tplCtx.beginPath();
tplCtx.arc(x, y, radius, 0, 2 * Math.PI, false);
tplCtx.fillStyle = 'rgba(0,0,0,1)';
tplCtx.fill();
} else {
var gradient = tplCtx.createRadialGradient(x, y, radius*blurFactor, x, y, radius);
gradient.addColorStop(0, 'rgba(0,0,0,1)');
gradient.addColorStop(1, 'rgba(0,0,0,0)');
tplCtx.fillStyle = gradient;
tplCtx.fillRect(0, 0, 2*radius, 2*radius);
}
return tplCanvas;
};
View Code
当 radius=50, blurFactor = 0.85 ,测试效果如下:
![](https://img2020.cnblogs.com/blog/910379/202112/910379-20211224105140342-2015963750.png)
当 radius=50, blurFactor = 0.5 ,测试效果如下:
![](https://img2020.cnblogs.com/blog/910379/202112/910379-20211224105208012-428969653.png)
3.2、线性色谱 _getColorPalette , 构建0到256的调色板
通过createLinearGradient你可以自主定制自己的热力图色谱(config.gradient)。
主要是调用 canvas 的 createLinearGradient 方法。核心方法是canvas的createLinearGradient方法。
var _getColorPalette = function(config) {
var gradientConfig = config.gradient || config.defaultGradient;
var paletteCanvas = document.createElement('canvas');
var paletteCtx = paletteCanvas.getContext('2d');
paletteCanvas.width = 256;
paletteCanvas.height = 1;
var gradient = paletteCtx.createLinearGradient(0, 0, 256, 1);
for (var key in gradientConfig) {
gradient.addColorStop(key, gradientConfig[key]);
}
paletteCtx.fillStyle = gradient;
paletteCtx.fillRect(0, 0, 256, 1);
return paletteCtx.getImageData(0, 0, 256, 1).data;
};
用 html canvas代码测试效果,代码如下:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
DOCTYPE html>
_getColorPalette
/*canvas {border:1px solid black;}*/
window.onload = function () {
var container = document.querySelector('.heatmap');
/*var HeatmapConfig = {
defaultRadius: 40,
defaultRenderer: 'canvas2d',
defaultGradient: { 0.25: "rgb(0,0,255)", 0.55: "rgb(0,255,0)", 0.85: "yellow", 1.0: "rgb(255,0,0)"},
defaultMaxOpacity: 1,
defaultMinOpacity: 0,
defaultBlur: .85,
defaultXField: 'x',
defaultYField: 'y',
defaultValueField: 'value',
plugins: {}
};*/
var config = { defaultGradient: { 0.25: "rgb(0,0,255)", 0.55: "rgb(0,255,0)", 0.85: "yellow", 1.0: "rgb(255,0,0)"} }
var imgData = _getColorPalette(config);
var c=document.createElement("canvas");
container.appendChild(c);
var ctx=c.getContext("2d");
var img = ctx.getImageData(0, 0, 256, 1)
//img.data = imgData;
for (let i = 0; i [0, 1]
var templateAlpha = (value-min)/(max-min);
// this fixes #176: small values are not visible because globalAlpha < .01 cannot be read from imageData
shadowCtx.globalAlpha = templateAlpha < .01 ? .01 : templateAlpha;
shadowCtx.drawImage(tpl, rectX, rectY);
// update renderBoundaries
if (rectX < this._renderBoundaries[0]) {
this._renderBoundaries[0] = rectX;
}
if (rectY < this._renderBoundaries[1]) {
this._renderBoundaries[1] = rectY;
}
if (rectX + 2*radius > this._renderBoundaries[2]) {
this._renderBoundaries[2] = rectX + 2*radius;
}
if (rectY + 2*radius > this._renderBoundaries[3]) {
this._renderBoundaries[3] = rectY + 2*radius;
}
}
},
用 html canvas代码测试效果,代码如下:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
DOCTYPE html>
_drawAlpha
/*canvas {border:1px solid black;}*/
window.onload = function () {
var container = document.querySelector('.heatmap');
var shadowCanvas = this.shadowCanvas = document.createElement('canvas');
this._width = shadowCanvas.width = 900;
this._height = shadowCanvas.height = 900;
this.shadowCtx = shadowCanvas.getContext('2d');
shadowCanvas.style.cssText = 'position:absolute;left:0;top:0;';
container.style.position = 'relative';
container.appendChild(shadowCanvas);
var renderBoundaries = this._renderBoundaries = [10000, 10000, 0, 0];
// this._min
// this._max
this._blur = 0.85;
this._templates = {};
var data = {
min: 0,
max: 10,
// x坐标, y坐标, value值, radius圆半径
data: [{x: 10, y: 15, value: 5, radius: 40}, {x: 130, y: 170, value: 8, radius: 40}, {x: 200, y: 250, value: 10, radius: 40},
{x: 300, y: 450, value: 5, radius: 40}, {x: 300, y: 450, value: 5, radius: 40}]
};
_drawAlpha(data);
}
var _getPointTemplate = function(radius, blurFactor) {
var tplCanvas = document.createElement('canvas');
var tplCtx = tplCanvas.getContext('2d');
var x = radius;
var y = radius;
tplCanvas.width = tplCanvas.height = radius*2;
if (blurFactor == 1) {
tplCtx.beginPath();
tplCtx.arc(x, y, radius, 0, 2 * Math.PI, false);
tplCtx.fillStyle = 'rgba(0,0,0,1)';
tplCtx.fill();
} else {
var gradient = tplCtx.createRadialGradient(x, y, radius*blurFactor, x, y, radius);
gradient.addColorStop(0, 'rgba(0,0,0,1)');
gradient.addColorStop(1, 'rgba(0,0,0,0)');
tplCtx.fillStyle = gradient;
tplCtx.fillRect(0, 0, 2*radius, 2*radius);
}
return tplCanvas;
};
var _drawAlpha = function(data) {
var min = this._min = data.min;
var max = this._max = data.max;
var data = data.data || [];
var dataLen = data.length;
// on a point basis?
var blur = 1 - this._blur;
while(dataLen--) {
var point = data[dataLen];
var x = point.x;
var y = point.y;
var radius = point.radius;
// if value is bigger than max
// use max as value
var value = Math.min(point.value, max);
var rectX = x - radius;
var rectY = y - radius;
var shadowCtx = this.shadowCtx;
var tpl;
if (!this._templates[radius]) {
this._templates[radius] = tpl = _getPointTemplate(radius, blur);
} else {
tpl = this._templates[radius];
}
// value from minimum / value range
// => [0, 1]
var templateAlpha = (value-min)/(max-min);
// this fixes #176: small values are not visible because globalAlpha < .01 cannot be read from imageData
shadowCtx.globalAlpha = templateAlpha maxWidth) {
width = maxWidth - x;
}
if (y + height > maxHeight) {
height = maxHeight - y;
}
var img = this.shadowCtx.getImageData(x, y, width, height);
var imgData = img.data;
var len = imgData.length;
var palette = this._palette;
for (var i = 3; i < len; i+= 4) {
var alpha = imgData[i];
var offset = alpha * 4;
if (!offset) {
continue;
}
var finalAlpha;
if (opacity > 0) {
finalAlpha = opacity;
} else {
if (alpha
_colorize
/*canvas {border:1px solid black;}*/
window.onload = function () {
var container = document.querySelector('.heatmap');
var shadowCanvas = this.shadowCanvas = document.createElement('canvas');
var canvas = this.canvas = document.createElement('canvas');
var renderBoundaries = this._renderBoundaries = [10000, 10000, 0, 0];
canvas.className = 'heatmap-canvas';
this._width = canvas.width = shadowCanvas.width = 900;
this._height = canvas.height = shadowCanvas.height = 900;
this.shadowCtx = shadowCanvas.getContext('2d');
this.ctx = canvas.getContext('2d');
canvas.style.cssText = shadowCanvas.style.cssText = 'position:absolute;left:0;top:0;';
container.style.position = 'relative';
container.appendChild(canvas);
var config = { defaultGradient: { 0.25: "rgb(0,0,255)", 0.55: "rgb(0,255,0)", 0.85: "yellow", 1.0: "rgb(255,0,0)"} }
this._palette = _getColorPalette(config);
this._templates = {};
var opacity = this._opacity = 0.8;
var maxOpacity = this._maxOpacity = 1;
var minOpacity = this._minOpacity = 0;
var useGradientOpacity = this._useGradientOpacity = true;
// this._min
// this._max
this._blur = 0.85;
var data = {
min: 0,
max: 10,
// x坐标, y坐标, value值, radius圆半径
data: [{x: 10, y: 15, value: 5, radius: 40}, {x: 130, y: 170, value: 8, radius: 40}, {x: 200, y: 250, value: 10, radius: 40},
{x: 300, y: 450, value: 5, radius: 40}, {x: 300, y: 450, value: 5, radius: 40}]
};
_drawAlpha(data);
_colorize();
};
var _getColorPalette = function(config) {
var gradientConfig = config.gradient || config.defaultGradient;
var paletteCanvas = document.createElement('canvas');
var paletteCtx = paletteCanvas.getContext('2d');
paletteCanvas.width = 256;
paletteCanvas.height = 1;
var gradient = paletteCtx.createLinearGradient(0, 0, 256, 1);
for (var key in gradientConfig) {
gradient.addColorStop(key, gradientConfig[key]);
}
paletteCtx.fillStyle = gradient;
paletteCtx.fillRect(0, 0, 256, 1);
return paletteCtx.getImageData(0, 0, 256, 1).data;
};
var _getPointTemplate = function(radius, blurFactor) {
var tplCanvas = document.createElement('canvas');
var tplCtx = tplCanvas.getContext('2d');
var x = radius;
var y = radius;
tplCanvas.width = tplCanvas.height = radius*2;
if (blurFactor == 1) {
tplCtx.beginPath();
tplCtx.arc(x, y, radius, 0, 2 * Math.PI, false);
tplCtx.fillStyle = 'rgba(0,0,0,1)';
tplCtx.fill();
} else {
var gradient = tplCtx.createRadialGradient(x, y, radius*blurFactor, x, y, radius);
gradient.addColorStop(0, 'rgba(0,0,0,1)');
gradient.addColorStop(1, 'rgba(0,0,0,0)');
tplCtx.fillStyle = gradient;
tplCtx.fillRect(0, 0, 2*radius, 2*radius);
}
return tplCanvas;
};
var _drawAlpha = function(data) {
var min = this._min = data.min;
var max = this._max = data.max;
var data = data.data || [];
var dataLen = data.length;
// on a point basis?
var blur = 1 - this._blur;
while(dataLen--) {
var point = data[dataLen];
var x = point.x;
var y = point.y;
var radius = point.radius;
// if value is bigger than max
// use max as value
var value = Math.min(point.value, max);
var rectX = x - radius;
var rectY = y - radius;
var shadowCtx = this.shadowCtx;
var tpl;
if (!this._templates[radius]) {
this._templates[radius] = tpl = _getPointTemplate(radius, blur);
} else {
tpl = this._templates[radius];
}
// value from minimum / value range
// => [0, 1]
var templateAlpha = (value-min)/(max-min);
// this fixes #176: small values are not visible because globalAlpha < .01 cannot be read from imageData
shadowCtx.globalAlpha = templateAlpha
div {
width: 100%;
height: 900px;
/*border-style:solid;*/
/*border-color:red;*/
border:2px solid Orange;
}
/*
h337”是heatmap.js注册的全局对象的名字。您可以使用它来创建热图实例
h337.create(configObject) 返回一个heatmapInstance。
使用 h337.create 创建热图实例。可以使用 configObject 自定义热图。
configObject 参数是必需的。
*/
var heatmap = h337.create({
// 可能的配置属性:
// container (DOMNode) *必需*
// A DOM node where the heatmap canvas should be appended (heatmap will adapt to the node's size)
// 应附加热图画布的 DOM 节点(热图将适应节点的大小)
container: document.getElementById("heatmap"),
// 背景色
// backgroundColor (string) *optional*
// A background color string in form of hexcode, color name, or rgb(a)
// 十六进制代码、颜色名称或 rgb(a) 形式的背景颜色字符串
//backgroundColor: "#f3f3f3",
//backgroundColor: "rgb(240, 240, 240)",
backgroundColor: "rgba(240, 240, 240, 0.2)",
// gradient (object) *可选*
// An object that represents the gradient (syntax: number string [0,1] : color string), check out the example
// 表示渐变的对象(语法:数字字符串[0,1]:颜色字符串),查看示例
gradient: { 0.25: "rgb(0,0,255)", 0.55: "rgb(0,255,0)", 0.85: "yellow", 1.0: "rgb(255,0,0)"},
// radius (number) *可选*
// The radius each datapoint will have (if not specified on the datapoint itself)
// 每个数据点将具有的半径(如果未在数据点本身上指定)
radius: 20,
// opacity (number) [0,1] *可选* default = .6
// A global opacity for the whole heatmap. This overrides maxOpacity and minOpacity if set!
// 整个热图的全局不透明度。如果设置,这将覆盖 maxOpacity 和 minOpacity!
// opacity: .6,
// maxOpacity (number) [0,1] *可选*
// The maximal opacity the highest value in the heatmap will have. (will be overridden if opacity set)
// 热图中最高值的最大不透明度。(如果设置不透明度将被覆盖)
maxOpacity: 1,
// minOpacity(number) [0,1] *可选*
// The minimum opacity the lowest value in the heatmap will have (will be overridden if opacity set)
// 热图中最低值的最小不透明度(如果设置了不透明度,将被覆盖)
minOpacity: 0,
// onExtremaChange function callback 函数回调
// Pass a callback to receive extrema change updates. Useful for DOM legends.
// 传递回调以接收极值更改更新。对 DOM 图例很有用。
// onExtremaChange
// blur (number) [0,1] *可选* default = 0.85
//
// 将应用于所有数据点的模糊因子。模糊系数越高,渐变就越平滑
// xField (string) *可选* default = "x"
//
// 数据点中 x 坐标的属性名称
xField: "x",
// yField (string) *optional* default = "y"
//
// 数据点中 y 坐标的属性名称
yField: "y",
// valueField (string) *optional* default = "value"
//
// 数据点中 y 坐标的属性名称
valueField: "value"
});
heatmap.setData({
min: 0,
max: 10,
data: [{x: 10, y: 15, value: 5, radius: 40}, {x: 130, y: 170, value: 8, radius: 40}, {x: 200, y: 250, value: 10, radius: 40},
{x: 300, y: 450, value: 5, radius: 40}, {x: 300, y: 450, value: 5, radius: 40}]
//data: [{ x: 50, y: 75, value: 5, radius: 40}, {x: 200, y: 350, value: 3, radius: 10}]
});
结果如下:
![](https://img2020.cnblogs.com/blog/910379/202112/910379-20211230163153075-419793408.png)
参考:
heatmap.js(热力图)源码解读
热点图heatMap.js V2.0 研究笔记
|